/* eslint-disable @typescript-eslint/no-this-alias */

// @ts-nocheck
//Taken from https://raw.githubusercontent.com/coopernurse/node-pool/6c98fa9163bbe35b683ffc2b55ac741d02956096/lib/generic-pool.js
//Version 3 of generic-pool has lots og bugs and node program will never finish.
//So I copied version 2.5.4 below

/**
 * @class
 * @private
 */
function PriorityQueue(size) {
	if (!(this instanceof PriorityQueue)) {
		return new PriorityQueue();
	}

	this._size = size;
	this._slots = null;
	this._total = null;

	// initialize arrays to hold queue elements
	size = Math.max(+size | 0, 1);
	this._slots = [];
	for (var i = 0; i < size; i += 1) {
		this._slots.push([]);
	}
}

PriorityQueue.prototype.size = function size() {
	if (this._total === null) {
		this._total = 0;
		for (var i = 0; i < this._size; i += 1) {
			this._total += this._slots[i].length;
		}
	}
	return this._total;
};

PriorityQueue.prototype.enqueue = function enqueue(obj, priority) {
	var priorityOrig;

	// Convert to integer with a default value of 0.
	priority = priority && +priority | 0 || 0;

	// Clear cache for total.
	this._total = null;
	if (priority) {
		priorityOrig = priority;
		if (priority < 0 || priority >= this._size) {
			priority = (this._size - 1);
			// put obj at the end of the line
			console.error('invalid priority: ' + priorityOrig + ' must be between 0 and ' + priority);
		}
	}

	this._slots[priority].push(obj);
};

PriorityQueue.prototype.dequeue = function dequeue(_callback) {
	var obj = null;
	// Clear cache for total.
	this._total = null;
	for (var i = 0, sl = this._slots.length; i < sl; i += 1) {
		if (this._slots[i].length) {
			obj = this._slots[i].shift();
			break;
		}
	}
	return obj;
};

function doWhileAsync(conditionFn, iterateFn, callbackFn) {
	var next = function() {
		if (conditionFn()) {
			iterateFn(next);
		} else {
			callbackFn();
		}
	};
	next();
}

/**
 * Generate an Object pool with a specified `factory`.
 *
 * @class
 * @param {Object} factory
 *   Factory to be used for generating and destorying the items.
 * @param {String} factory.name
 *   Name of the factory. Serves only logging purposes.
 * @param {Function} factory.create
 *   Should create the item to be acquired,
 *   and call it's first callback argument with the generated item as it's argument.
 * @param {Function} factory.destroy
 *   Should gently close any resources that the item is using.
 *   Called before the items is destroyed.
 * @param {Function} factory.validate
 *   Should return true if connection is still valid and false
 *   If it should be removed from pool. Called before item is
 *   acquired from pool.
 * @param {Function} factory.validateAsync
 *   Asynchronous validate function. Receives a callback function
 *   as its second argument, that should be called with a single
 *   boolean argument being true if the item is still valid and false
 *   if it should be removed from pool. Called before item is
 *   acquired from pool. Only one of validate/validateAsync may be specified
 * @param {Number} factory.max
 *   Maximum number of items that can exist at the same time.  Default: 1.
 *   Any further acquire requests will be pushed to the waiting list.
 * @param {Number} factory.min
 *   Minimum number of items in pool (including in-use). Default: 0.
 *   When the pool is created, or a resource destroyed, this minimum will
 *   be checked. If the pool resource count is below the minimum, a new
 *   resource will be created and added to the pool.
 * @param {Number} factory.idleTimeoutMillis
 *   Delay in milliseconds after the idle items in the pool will be destroyed.
 *   And idle item is that is not acquired yet. Waiting items doesn't count here.
 * @param {Number} factory.reapIntervalMillis
 *   Cleanup is scheduled in every `factory.reapIntervalMillis` milliseconds.
 * @param {Boolean|Function} factory.log
 *   Whether the pool should log activity. If function is specified,
 *   that will be used instead. The function expects the arguments msg, loglevel
 * @param {Number} factory.priorityRange
 *   The range from 1 to be treated as a valid priority
 * @param {RefreshIdle} factory.refreshIdle
 *   Should idle resources at or below the min threshold be destroyed and recreated every idleTimeoutMillis? Default: true.
 * @param {Bool} [factory.returnToHead=false]
 *   Returns released object to head of available objects list
 */
function Pool(factory) {
	if (!(this instanceof Pool)) {
		return new Pool(factory);
	}

	if (factory.validate && factory.validateAsync) {
		throw new Error('Only one of validate or validateAsync may be specified');
	}

	// defaults
	factory.idleTimeoutMillis = factory.idleTimeoutMillis || 30000;
	factory.returnToHead = factory.returnToHead || false;
	factory.refreshIdle = ('refreshIdle' in factory) ? factory.refreshIdle : true;
	factory.reapInterval = factory.reapIntervalMillis || 1000;
	factory.priorityRange = factory.priorityRange || 1;
	factory.validate = factory.validate || function() {
		return true;
	};

	factory.max = parseInt(factory.max, 10);
	factory.min = parseInt(factory.min, 10);

	factory.max = Math.max(isNaN(factory.max) ? 1 : factory.max, 1);
	factory.min = Math.min(isNaN(factory.min) ? 0 : factory.min, factory.max - 1);

	this._factory = factory;
	this._inUseObjects = [];
	this._draining = false;
	this._waitingClients = new PriorityQueue(factory.priorityRange);
	this._availableObjects = [];
	this._asyncTestObjects = [];
	this._count = 0;
	this._removeIdleTimer = null;
	this._removeIdleScheduled = false;

	// create initial resources (if factory.min > 0)
	this._ensureMinimum();
}

/**
 * logs to console or user defined log function
 * @private
 * @param {string} str
 * @param {string} level
 */
Pool.prototype._log = function log(str, level) {
	if (typeof this._factory.log === 'function') {
		this._factory.log(str, level);
	} else if (this._factory.log) {
		console.log(level.toUpperCase() + ' pool ' + this._factory.name + ' - ' + str);
	}
};

/**
 * Request the client to be destroyed. The factory's destroy handler
 * will also be called.
 *
 * This should be called within an acquire() block as an alternative to release().
 *
 * @param {Object} obj
 *   The acquired item to be destoyed.
 * @param {Function} callback
 *   Optional. Callback invoked after client is destroyed
 */
Pool.prototype.destroy = function destroy(obj, cb) {
	this._count -= 1;
	if (this._count < 0) this._count = 0;
	this._availableObjects = this._availableObjects.filter(function(objWithTimeout) {
		return (objWithTimeout.obj !== obj);
	});

	this._inUseObjects = this._inUseObjects.filter(function(objInUse) {
		return (objInUse !== obj);
	});

	this._factory.destroy(obj, cb);

	// keep compatibily with old interface
	if (this._factory.destroy.length === 1 && cb && typeof cb === 'function') {
		cb();
	}

	this._ensureMinimum();
};

/**
 * Checks and removes the available (idle) clients that have timed out.
 * @private
 */
Pool.prototype._removeIdle = function removeIdle() {
	var toRemove = [];
	var now = new Date().getTime();
	var i;
	var al = this._availableObjects.length;
	var refreshIdle = this._factory.refreshIdle;
	var maxRemovable = this._count - this._factory.min;
	var timeout;

	this._removeIdleScheduled = false;

	// Go through the available (idle) items,
	// check if they have timed out
	for (i = 0; i < al && (refreshIdle || (maxRemovable > toRemove.length)); i++) {
		timeout = this._availableObjects[i].timeout;
		if (now >= timeout) {
			// Client timed out, so destroy it.
			this._log('removeIdle() destroying obj - now:' + now + ' timeout:' + timeout, 'verbose');
			toRemove.push(this._availableObjects[i].obj);
		}
	}

	toRemove.forEach(this.destroy, this);

	// NOTE: we are re-calcing this value because it may have changed
	// after destroying items above
	// Replace the available items with the ones to keep.
	al = this._availableObjects.length;

	if (al > 0) {
		this._log('this._availableObjects.length=' + al, 'verbose');
		this._scheduleRemoveIdle();
	} else {
		this._log('removeIdle() all objects removed', 'verbose');
	}
};

/**
 * Schedule removal of idle items in the pool.
 *
 * More schedules cannot run concurrently.
 */
Pool.prototype._scheduleRemoveIdle = function scheduleRemoveIdle() {
	var self = this;
	if (!this._removeIdleScheduled) {
		this._removeIdleScheduled = true;
		this._removeIdleTimer = setTimeout(function() {
			self._removeIdle();
		}, this._factory.reapInterval);
	}
};

/**
 * Try to get a new client to work, and clean up pool unused (idle) items.
 *
 *  - If there are available clients waiting, shift the first one out (LIFO),
 *    and call its callback.
 *  - If there are no waiting clients, try to create one if it won't exceed
 *    the maximum number of clients.
 *  - If creating a new client would exceed the maximum, add the client to
 *    the wait list.
 * @private
 */
Pool.prototype._dispense = function dispense() {
	var self = this;
	var objWithTimeout = null;
	var clientCb = null;
	var waitingCount = this._waitingClients.size();

	this._log('dispense() clients=' + waitingCount + ' available=' + this._availableObjects.length, 'info');

	if (waitingCount < 1) {
		return;
	}

	if (this._factory.validateAsync) {
		doWhileAsync(function() {
			return self._availableObjects.length > 0;
		},
		this._createAsyncValidator(),
		function() {
			if (self._count < self._factory.max) {
				self._createResource();
			}
		});

		return;
	}

	while (this._availableObjects.length > 0) {
		this._log('dispense() - reusing obj', 'verbose');
		objWithTimeout = this._availableObjects[0];
		if (!this._factory.validate(objWithTimeout.obj)) {
			this.destroy(objWithTimeout.obj);
			continue;
		}

		this._availableObjects.shift();
		this._inUseObjects.push(objWithTimeout.obj);
		clientCb = this._waitingClients.dequeue();
		return clientCb(null, objWithTimeout.obj);
	}

	if (this._count < this._factory.max) {
		this._createResource();
	}
};

Pool.prototype._createAsyncValidator = function _createAsyncValidator() {
	var self = this;
	return function asyncValidate(next) {
		self._log('dispense() - reusing obj', 'verbose');

		var objWithTimeout = self._availableObjects.shift();
		self._asyncTestObjects.push(objWithTimeout);

		self._factory.validateAsync(objWithTimeout.obj, function(valid) {
			var pos = self._asyncTestObjects.indexOf(objWithTimeout);
			self._asyncTestObjects.splice(pos, 1);

			if (!valid) {
				self.destroy(objWithTimeout.obj);
				return next();
			}
			if (self._waitingClients.size() < 1) {
				// there is no longer anyone waiting for a resource
				self._addResourceToAvailableObjects(objWithTimeout.obj);
				return;
			}

			self._inUseObjects.push(objWithTimeout.obj);
			var clientCb = self._waitingClients.dequeue();
			clientCb(null, objWithTimeout.obj);
		});
	};
};

/**
 * @private
 */
Pool.prototype._createResource = function _createResource() {
	this._count += 1;
	this._log('createResource() - creating obj - count=' + this._count + ' min=' + this._factory.min + ' max=' + this._factory.max, 'verbose');
	var self = this;
	this._factory.create(function() {
		var err, obj;
		var clientCb = self._waitingClients.dequeue();
		if (arguments.length > 1) {
			err = arguments[0];
			obj = arguments[1];
		} else {
			err = (arguments[0] instanceof Error) ? arguments[0] : null;
			obj = (arguments[0] instanceof Error) ? null : arguments[0];
		}
		if (err) {
			self._count -= 1;
			if (self._count < 0) self._count = 0;
			if (clientCb) {
				clientCb(err, obj);
			}
			process.nextTick(function() {
				self._dispense();
			});
		} else {
			self._inUseObjects.push(obj);
			if (clientCb) {
				clientCb(err, obj);
			} else {
				self._addResourceToAvailableObjects(obj);
			}
		}
	});
};

Pool.prototype._addResourceToAvailableObjects = function(obj) {
	var objWithTimeout = {
		obj: obj,
		timeout: (new Date().getTime() + this._factory.idleTimeoutMillis)
	};

	if (this._factory.returnToHead) {
		this._availableObjects.splice(0, 0, objWithTimeout);
	} else {
		this._availableObjects.push(objWithTimeout);
	}

	this._dispense();
	this._scheduleRemoveIdle();
};

/**
 * @private
 */
Pool.prototype._ensureMinimum = function _ensureMinimum() {
	var i, diff;
	if (!this._draining && (this._count < this._factory.min)) {
		diff = this._factory.min - this._count;
		for (i = 0; i < diff; i++) {
			this._createResource();
		}
	}
};

/**
 * Request a new client. The callback will be called,
 * when a new client will be availabe, passing the client to it.
 *
 * @param {Function} callback
 *   Callback function to be called after the acquire is successful.
 *   The function will receive the acquired item as the first parameter.
 *
 * @param {Number} priority
 *   Optional.  Integer between 0 and (priorityRange - 1).  Specifies the priority
 *   of the caller if there are no available resources.  Lower numbers mean higher
 *   priority.
 *
 * @returns {boolean} `true` if the pool is not fully utilized, `false` otherwise.
 */
Pool.prototype.acquire = function acquire(callback, priority) {
	if (this._draining) {
		throw new Error('pool is draining and cannot accept work');
	}
	if (process.domain) {
		callback = process.domain.bind(callback);
	}
	this._waitingClients.enqueue(callback, priority);
	this._dispense();
	return (this._count < this._factory.max);
};

/**
 * @deprecated
 */
Pool.prototype.borrow = function borrow(callback, priority) {
	this._log('borrow() is deprecated. use acquire() instead', 'warn');
	this.acquire(callback, priority);
};

/**
 * Return the client to the pool, in case it is no longer required.
 *
 * @param {Object} obj
 *   The acquired object to be put back to the pool.
 */
Pool.prototype.release = function release(obj) {
	// check to see if this object has already been released (i.e., is back in the pool of this._availableObjects)
	if (this._availableObjects.some(function(objWithTimeout) {
		return (objWithTimeout.obj === obj);
	})) {
		this._log('release called twice for the same resource: ' + (new Error().stack), 'error');
		return;
	}

	// check to see if this object exists in the `in use` list and remove it
	var index = this._inUseObjects.indexOf(obj);
	if (index < 0) {
		this._log('attempt to release an invalid resource: ' + (new Error().stack), 'error');
		return;
	}

	// this._log("return to pool")
	this._inUseObjects.splice(index, 1);
	this._addResourceToAvailableObjects(obj);
};

/**
 * @deprecated
 */
Pool.prototype.returnToPool = function returnToPool(obj) {
	this._log('returnToPool() is deprecated. use release() instead', 'warn');
	this.release(obj);
};

function invoke(cb) {
	if (typeof setImmediate === 'function') {
		setImmediate(cb);
	} else {
		setTimeout(cb, 0);
	}
}

/**
 * Disallow any new requests and let the request backlog dissapate.
 *
 * @param {Function} callback
 *   Optional. Callback invoked when all work is done and all clients have been
 *   released.
 */
Pool.prototype.drain = function drain(callback) {
	this._log('draining', 'info');

	// disable the ability to put more work on the queue.
	this._draining = true;

	var self = this;
	var check = function() {
		if (self._waitingClients.size() > 0) {
			// wait until all client requests have been satisfied.
			return setTimeout(check, 100);
		}
		if (self._asyncTestObjects.length > 0) {
			// wait until any async tests have finished
			return setTimeout(check, 100);
		}
		if (self._availableObjects.length !== self._count) {
			// wait until in use object have been released.
			return setTimeout(check, 100);
		}
		if (callback) {
			invoke(callback);
		}
	};
	check();
};

/**
 * Forcibly destroys all clients regardless of timeout.  Intended to be
 * invoked as part of a drain.  Does not prevent the creation of new
 * clients as a result of subsequent calls to acquire.
 *
 * Note that if factory.min > 0, the pool will destroy all idle resources
 * in the pool, but replace them with newly created resources up to the
 * specified factory.min value.  If this is not desired, set factory.min
 * to zero before calling destroyAllNow()
 *
 * @param {Function} callback
 *   Optional. Callback invoked after all existing clients are destroyed.
 */
Pool.prototype.destroyAllNow = function destroyAllNow(callback) {
	this._log('force destroying all objects', 'info');
	var willDie = this._availableObjects;
	this._availableObjects = [];
	var todo = willDie.length;
	var done = 0;
	var obj = willDie.shift();

	this._removeIdleScheduled = false;
	clearTimeout(this._removeIdleTimer);

	if (todo === 0 && callback) {
		invoke(callback);
		return;
	}
	while (obj !== null && obj !== undefined) {
		this.destroy(obj.obj, function() {
			++done;
			if (done === todo && callback) {
				invoke(callback);
				return;
			}
		});
		obj = willDie.shift();
	}
};

/**
 * Decorates a function to use a acquired client from the object pool when called.
 *
 * @param {Function} decorated
 *   The decorated function, accepting a client as the first argument and
 *   (optionally) a callback as the final argument.
 *
 * @param {Number} priority
 *   Optional.  Integer between 0 and (priorityRange - 1).  Specifies the priority
 *   of the caller if there are no available resources.  Lower numbers mean higher
 *   priority.
 */
Pool.prototype.pooled = function pooled(decorated, priority) {
	var self = this;
	return function() {
		var callerArgs = arguments;
		var callerCallback = callerArgs[callerArgs.length - 1];
		var callerHasCallback = typeof callerCallback === 'function';
		self.acquire(function(err, client) {
			if (err) {
				if (callerHasCallback) {
					callerCallback(err);
				}
				return;
			}

			var args = [client].concat(Array.prototype.slice.call(callerArgs, 0, callerHasCallback ? -1 : undefined));
			args.push(function() {
				self.release(client);
				if (callerHasCallback) {
					callerCallback.apply(null, arguments);
				}
			});

			decorated.apply(null, args);
		}, priority);
	};
};

Pool.prototype.getPoolSize = function getPoolSize() {
	return this._count;
};

Pool.prototype.getName = function getName() {
	return this._factory.name;
};

Pool.prototype.availableObjectsCount = function availableObjectsCount() {
	return this._availableObjects.length;
};

Pool.prototype.inUseObjectsCount = function inUseObjectsCount() {
	return this._inUseObjects.length;
};

Pool.prototype.waitingClientsCount = function waitingClientsCount() {
	return this._waitingClients.size();
};

Pool.prototype.getMaxPoolSize = function getMaxPoolSize() {
	return this._factory.max;
};

Pool.prototype.getMinPoolSize = function getMinPoolSize() {
	return this._factory.min;
};

exports.Pool = Pool;